#ifndef GMAP_NDARRAY_
#define GMAP_NDARRAY_

#include <cstring>

#include <memory>
#include <type_traits>
#include <utility>

template <typename T>
struct array_info
{
    static const size_t elems = 1;
};

template <typename T, unsigned Idx, unsigned Curr>
struct array_dim_info
{
    constexpr static
    size_t get_size()
    {
        return 0;
    }
};

template <typename T, unsigned Size, unsigned Idx, unsigned Curr>
struct array_dim_info<T[Size], Idx, Curr>
{
    constexpr static
    size_t get_size()
    {
        return Idx == Curr? Size :
                            array_dim_info<T, Idx, Curr + 1>::get_size();
    }
};

template <typename T, size_t Size>
struct array_info<T[Size]>
{
    using array_type = T[Size];
    using base_type = typename std::remove_all_extents<array_type>::type;

    static const size_t elems = Size * array_info<T>::elems;
    const static size_t dims  = std::rank<array_type>::value;

    template <unsigned Idx>
    constexpr static
    size_t get_size()
    {
        return array_dim_info<array_type, Idx, 0>::get_size();
    }
};

template <typename T>
struct array_get
{};

template <typename T, size_t Size>
struct array_get<T[Size]>
{
    using array_type = T[Size];

    template <typename... Args>
    static
    typename std::remove_all_extents<T>::type &
    get(array_type &a, int i, Args... args)
    {
        return array_get<T>::get(a[i], args...);
    }

    template <typename... Args>
    static
    typename std::remove_all_extents<T>::type &
    get(array_type &a, int i)
    {
        return a[i];
    }

    template <typename... Args>
    static
    const typename std::remove_all_extents<T>::type &
    get(const array_type &a, int i, Args... args)
    {
        return array_get<T>::get(a[i], args...);
    }

    template <typename... Args>
    static
    const typename std::remove_all_extents<T>::type &
    get(const array_type &a, int i)
    {
        return a[i];
    }

};

template <typename T>
struct array
{
    static_assert(std::is_array<T>::value, "Type must be an array");
};

template <typename T>
using ref = T&;

template <typename T>
class array_ref
{
};

template <typename T>
class const_array_ref
{
};

template <typename T, size_t Size>
class array<T[Size]>;

template <typename T, size_t Size>
class array_ref<T[Size]> :
    public array_info<T[Size]> {
    using array_type = T[Size];

private:
    array_type &data_;

public:
    array_ref(array<array_type> &a);

    array_ref(array_type &a) :
        data_(a)
    {
    }

    inline
    T &operator[](int i)
    {
        return data_[i];
    }

    inline
    const T &operator[](int i) const
    {
        return data_[i];
    }
};

template <typename T, size_t Size>
class const_array_ref<T[Size]> :
    public array_info<T[Size]> {
    using array_type = T[Size];

private:
    const array_type &data_;

public:
    explicit const_array_ref(const array<array_type> &a);

    explicit const_array_ref(const const_array_ref &ref) :
        data_(ref.data_)
    {
    }

    explicit const_array_ref(const array_type &a) :
        data_(a)
    {
    }

    inline
    T &operator[](int i)
    {
        return data_[i];
    }

    inline
    const T &operator[](int i) const
    {
        return data_[i];
    }
};

template <typename T, size_t Size>
class array<T[Size]> :
    public array_info<T[Size]> {
    using array_type = T[Size];

    friend class array_ref<array_type>;

    using parent_info = array_info<array_type>;

private:
    std::unique_ptr<array_type> data_;

    static array_type *
    alloc()
    {
        return (array_type *)
            new typename parent_info::base_type[parent_info::elems];
    }

public:
    array() :
        data_(array::alloc())
    {
    }

    array(const array &a) :
        data_(array::alloc())
    {
        ::memcpy(data_.get(), a.data_.get(),
                 parent_info::elems * sizeof(array::base_type));
    }

    array(array &&a) :
        data_(std::move(a.data_))
    {
    }

    template <typename T2>
    array_ref<T2> reshape()
    {
        static_assert(parent_info::elems == array_info<T2>::elems,
                      "Total numer of elements do not match");
        static_assert(std::is_same<typename std::remove_all_extents<T2>::type,
                                   typename parent_info::base_type>::value,
                      "Base types do not match");
        return array_ref<T2>(*(T2 *) data_.get());
    }

    bool operator==(const array &a) const
    {
        if (this != &a) {
            typename parent_info::base_type *
                current = (typename parent_info::base_type *) data_.get();
            typename parent_info::base_type *
                other = (typename parent_info::base_type *) a.data_.get();

            return std::equal(current, current + parent_info::elems,
                              other);
        } else {
            return true;
        }
    }

    // Move assignment operator
    array &operator=(array &&a)
    {
        if (this != &a) {
            data_.reset(a.data_.release());
        }

        return *this;
    }

    // Assignment operator
    array &operator=(const array &a)
    {
        if (this != &a) {
            ::memcpy(data_.get(), a.data_.get(),
                     parent_info::elems * sizeof(array::base_type));
        }

        return *this;
    }

    T &operator[](int i)
    {
        return (*data_)[i];
    }

    const T &operator[](int i) const
    {
        return (*data_)[i];
    }

    template <typename... Args>
    typename parent_info::base_type &
    operator()(Args... args)
    {
        static_assert((sizeof...(Args)) == parent_info::dims, "Wrong number of indexes");
        return array_get<array_type>::get((*data_), args...);
    }

    template <typename... Args>
    const typename parent_info::base_type &
    operator()(Args... args) const
    {
        static_assert((sizeof...(Args)) == parent_info::dims, "Wrong number of indexes");
        return array_get<array_type>::get((*data_), args...);
    }

    template <unsigned Idx>
    constexpr static
    size_t get_size()
    {
        return parent_info::get_size<Idx>();
    }
};

template <typename T, size_t Size>
array_ref<T[Size]>::array_ref(array<T[Size]> &a) :
    data_(*a.data_.get())
{
}

template <typename T, size_t Size>
const_array_ref<T[Size]>::const_array_ref(const array<T[Size]> &a) :
    data_(*a.data_.get())
{
}

#endif

/* vim:set ft=cpp backspace=2 tabstop=4 shiftwidth=4 textwidth=120 foldmethod=marker expandtab: */
